Schritt-für-Schritt Tutorial
Von der Installation bis zur fertigen REST-API
3 Models, ViewSets, Custom Actions, Filtering
movieapi/
├── movieapi/
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── movies/
│ ├── __init__.py
│ ├── models.py # ✅ Models vorhanden
│ ├── admin.py
│ ├── views.py # ← Hier arbeiten wir!
│ └── apps.py
└── manage.py
# Neues Projekt erstellen:
django-admin startproject movieapi .
python manage.py startapp movies
# Models kopieren (wie in der Aufgabe gegeben)
# Dann Migrationen:
python manage.py makemigrations
python manage.py migrate
# Terminal (virtuelle Umgebung aktiviert):
pip install djangorestframework
# Output:
Collecting djangorestframework
Downloading djangorestframework-3.14.0-py3-none-any.whl
Installing collected packages: djangorestframework
Successfully installed djangorestframework-3.14.0
# Terminal:
pip show djangorestframework
# Output:
Name: djangorestframework
Version: 3.14.0
Summary: Web APIs for Django, made easy.
Home-page: https://www.django-rest-framework.org/
Author: Tom Christie
License: BSD
Django Rest Framework ist jetzt installiert.
# filepath: movieapi/settings.py
# ...existing code...
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Third-party Apps
'rest_framework', # ← NEU! DRF hinzufügen
# Eigene Apps
'movies', # ← Sollte bereits vorhanden sein
]
# ...existing code...
# filepath: movieapi/settings.py
# ...existing code...
# Django Rest Framework Configuration
REST_FRAMEWORK = {
# Permissions (für Development erstmal offen)
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny',
],
# Renderer (JSON + Browsable API)
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
'rest_framework.renderers.BrowsableAPIRenderer',
],
# Pagination
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10,
}
Konvertieren Models ↔ JSON
# filepath: movies/serializers.py
from rest_framework import serializers
from .models import Movie, Artist, MovieCasting
class MovieSerializer(serializers.ModelSerializer):
"""
Serializer für Movie Model
Konvertiert Movie-Objekte zu JSON und umgekehrt
"""
class Meta:
model = Movie
fields = '__all__' # Alle Felder
read_only_fields = ['created_at', 'updated_at']
class ArtistSerializer(serializers.ModelSerializer):
"""
Serializer für Artist Model
Inkludiert das full_name Property
"""
full_name = serializers.ReadOnlyField() # Property aus Model
class Meta:
model = Artist
fields = '__all__'
read_only_fields = ['created_at', 'updated_at']
class MovieCastingSerializer(serializers.ModelSerializer):
"""
Serializer für MovieCasting Model
Einfache Version nur mit IDs
"""
class Meta:
model = MovieCasting
fields = '__all__'
read_only_fields = ['created_at']
class MovieCastingDetailSerializer(serializers.ModelSerializer):
"""
Detaillierter Casting Serializer
Mit verschachtelten Movie & Artist Objekten
"""
movie = MovieSerializer(read_only=True)
artist = ArtistSerializer(read_only=True)
class Meta:
model = MovieCasting
fields = '__all__'
read_only_fields = ['created_at']
# Python Model:
movie = Movie.objects.get(pk=1)
# Movie(title="The Matrix", year=1999)
# Serializer:
serializer = MovieSerializer(movie)
# JSON Output:
serializer.data
# {
# "id": 1,
# "title": "The Matrix",
# "year": 1999,
# "genre": "Sci-Fi",
# "rating": "8.7",
# "description": "...",
# "created_at": "2024-01-15T10:00:00Z",
# "updated_at": "2024-01-15T10:00:00Z"
# }
# JSON Input:
data = {
"title": "Inception",
"year": 2010,
"genre": "Sci-Fi",
"rating": "8.8",
"description": "A thief..."
}
# Serializer:
serializer = MovieSerializer(data=data)
# Validierung:
if serializer.is_valid():
# Speichern:
movie = serializer.save()
# Movie in DB gespeichert!
else:
# Fehler anzeigen:
print(serializer.errors)
Minimaler Code, maximale Funktionalität
# filepath: movies/views.py
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import Movie, Artist, MovieCasting
from .serializers import (
MovieSerializer,
ArtistSerializer,
MovieCastingSerializer,
MovieCastingDetailSerializer
)
class MovieViewSet(viewsets.ModelViewSet):
"""
ViewSet für Movie Model
Automatisch verfügbar:
- list: GET /api/movies/
- create: POST /api/movies/
- retrieve: GET /api/movies/{id}/
- update: PUT /api/movies/{id}/
- partial_update: PATCH /api/movies/{id}/
- destroy: DELETE /api/movies/{id}/
"""
queryset = Movie.objects.all()
serializer_class = MovieSerializer
class ArtistViewSet(viewsets.ModelViewSet):
"""
ViewSet für Artist Model
Alle CRUD-Operationen automatisch verfügbar
"""
queryset = Artist.objects.all()
serializer_class = ArtistSerializer
class MovieCastingViewSet(viewsets.ModelViewSet):
"""
ViewSet für MovieCasting Model
Alle CRUD-Operationen automatisch verfügbar
"""
queryset = MovieCasting.objects.all()
serializer_class = MovieCastingSerializer
Mit diesen 3 ViewSets haben wir bereits 18 Endpoints erstellt!
6 Endpoints × 3 Models = 18 Endpoints (list, create, retrieve, update, partial_update, destroy)
# filepath: movies/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import MovieViewSet, ArtistViewSet, MovieCastingViewSet
# Router erstellen
router = DefaultRouter()
# ViewSets registrieren
router.register(r'movies', MovieViewSet, basename='movie')
router.register(r'artists', ArtistViewSet, basename='artist')
router.register(r'castings', MovieCastingViewSet, basename='casting')
# URLs
urlpatterns = [
path('', include(router.urls)),
]
# filepath: movieapi/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('movies.urls')), # ← NEU! API-URLs einbinden
]
# Movies:
GET /api/movies/ # Liste aller Filme
POST /api/movies/ # Film erstellen
GET /api/movies/{id}/ # Film-Details
PUT /api/movies/{id}/ # Film aktualisieren (komplett)
PATCH /api/movies/{id}/ # Film aktualisieren (teilweise)
DELETE /api/movies/{id}/ # Film löschen
# Artists:
GET /api/artists/ # Liste aller Künstler
POST /api/artists/ # Künstler erstellen
# ... (gleiche wie Movies)
# Castings:
GET /api/castings/ # Liste aller Besetzungen
POST /api/castings/ # Besetzung erstellen
# ... (gleiche wie Movies)
# Terminal:
python manage.py runserver
# Output:
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
January 15, 2024 - 10:00:00
Django version 4.2.7, using settings 'movieapi.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.
Browser öffnen: http://127.0.0.1:8000/api/
Du siehst die DRF Browsable API (HTML-Interface)
# Browser:
http://127.0.0.1:8000/api/movies/ # ✅ Leere Liste
http://127.0.0.1:8000/api/artists/ # ✅ Leere Liste
http://127.0.0.1:8000/api/castings/ # ✅ Leere Liste
# Output (JSON):
[]
Listen sind leer, da noch keine Daten in DB
http://127.0.0.1:8000/api/movies/title: The Matrix
year: 1999
genre: Sci-Fi
rating: 8.7
description: A computer hacker learns...
http://127.0.0.1:8000/api/artists/first_name: Keanu
last_name: Reeves
birth_date: 1964-09-02
nationality: Canadian
http://127.0.0.1:8000/api/castings/movie: 1 (The Matrix)
artist: 1 (Keanu Reeves)
role_name: Neo
is_main_role: true
order: 1
# filepath: movies/views.py
class MovieViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
@action(detail=False, methods=['get'])
def top_rated(self, request):
"""
Custom Endpoint: GET /api/movies/top_rated/
Liefert die Top 10 bestbewerteten Filme
"""
movies = Movie.objects.filter(
rating__isnull=False
).order_by('-rating')[:10]
serializer = self.get_serializer(movies, many=True)
return Response(serializer.data)
@action(detail=False, methods=['get'])
def recent(self, request):
"""
Custom Endpoint: GET /api/movies/recent/
Liefert Filme der letzten 5 Jahre
"""
from datetime import datetime
current_year = datetime.now().year
movies = Movie.objects.filter(year__gte=current_year - 5)
serializer = self.get_serializer(movies, many=True)
return Response(serializer.data)
@action(detail=True, methods=['get'])
def castings(self, request, pk=None):
"""
Custom Endpoint: GET /api/movies/{id}/castings/
Liefert die Besetzung eines Films
"""
movie = self.get_object()
castings = movie.castings.all()
serializer = MovieCastingDetailSerializer(castings, many=True)
return Response(serializer.data)
# filepath: movies/views.py
class ArtistViewSet(viewsets.ModelViewSet):
queryset = Artist.objects.all()
serializer_class = ArtistSerializer
@action(detail=True, methods=['get'])
def filmography(self, request, pk=None):
"""
Custom Endpoint: GET /api/artists/{id}/filmography/
Liefert alle Filme eines Künstlers
"""
artist = self.get_object()
castings = artist.movie_roles.all()
serializer = MovieCastingDetailSerializer(castings, many=True)
return Response(serializer.data)
@action(detail=True, methods=['get'])
def movies(self, request, pk=None):
"""
Custom Endpoint: GET /api/artists/{id}/movies/
Liefert nur die Filme (ohne Casting-Details)
"""
artist = self.get_object()
movies = Movie.objects.filter(castings__artist=artist).distinct()
serializer = MovieSerializer(movies, many=True)
return Response(serializer.data)
# Movies:
GET /api/movies/top_rated/ # Top 10 Filme
GET /api/movies/recent/ # Filme der letzten 5 Jahre
GET /api/movies/{id}/castings/ # Besetzung eines Films
# Artists:
GET /api/artists/{id}/filmography/ # Alle Rollen eines Künstlers
GET /api/artists/{id}/movies/ # Alle Filme eines Künstlers
# Terminal:
pip install django-filter
# Output:
Successfully installed django-filter-23.5
# filepath: movieapi/settings.py
INSTALLED_APPS = [
# ...existing...
'rest_framework',
'django_filters', # ← NEU!
'movies',
]
# ...existing...
REST_FRAMEWORK = {
# ...existing...
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
'rest_framework.filters.OrderingFilter',
],
}
# filepath: movies/views.py
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters
class MovieViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
# Filter Backends
filter_backends = [
DjangoFilterBackend,
filters.SearchFilter,
filters.OrderingFilter
]
# Welche Felder können gefiltert werden?
filterset_fields = ['year', 'genre']
# Welche Felder können durchsucht werden?
search_fields = ['title', 'description']
# Welche Felder können sortiert werden?
ordering_fields = ['year', 'rating', 'title']
# Standard-Sortierung
ordering = ['-year']
# ...existing actions...
# Jahr 2010:
GET /api/movies/?year=2010
# Genre Sci-Fi:
GET /api/movies/?genre=Sci-Fi
# Kombiniert:
GET /api/movies/?year=2010&genre=Action
# Titel enthält "Matrix":
GET /api/movies/?search=Matrix
# Beschreibung enthält "action":
GET /api/movies/?search=action
# Nach Jahr aufsteigend:
GET /api/movies/?ordering=year
# Nach Rating absteigend:
GET /api/movies/?ordering=-rating
# Mehrfach:
GET /api/movies/?ordering=-rating,title
# Sci-Fi Filme ab 2010,
# sortiert nach Rating:
GET /api/movies/?genre=Sci-Fi&year=2010&ordering=-rating
# Suche "Matrix",
# nur ab 1999:
GET /api/movies/?search=Matrix&year=1999
Öffne: http://127.0.0.1:8000/api/movies/?year=1999
Oben rechts im Browsable API siehst du "Filters"-Button!
# filepath: movies/admin.py
from django.contrib import admin
from .models import Movie, Artist, MovieCasting
@admin.register(Movie)
class MovieAdmin(admin.ModelAdmin):
"""Admin für Movie Model"""
list_display = ['title', 'year', 'genre', 'rating', 'created_at']
list_filter = ['year', 'genre']
search_fields = ['title', 'description']
ordering = ['-year', 'title']
@admin.register(Artist)
class ArtistAdmin(admin.ModelAdmin):
"""Admin für Artist Model"""
list_display = ['full_name', 'nationality', 'birth_date', 'created_at']
list_filter = ['nationality']
search_fields = ['first_name', 'last_name']
ordering = ['last_name', 'first_name']
@admin.register(MovieCasting)
class MovieCastingAdmin(admin.ModelAdmin):
"""Admin für MovieCasting Model"""
list_display = ['movie', 'artist', 'role_name', 'is_main_role', 'order']
list_filter = ['is_main_role', 'movie']
search_fields = ['role_name', 'movie__title', 'artist__last_name']
ordering = ['order']
# Terminal:
python manage.py createsuperuser
# Input:
Username: admin
Email: admin@example.com
Password: ***
Password (again): ***
# Output:
Superuser created successfully.
Browser: http://127.0.0.1:8000/admin/
Login mit: admin / dein Passwort
# GET Liste:
curl http://127.0.0.1:8000/api/movies/
# GET Details:
curl http://127.0.0.1:8000/api/movies/1/
# POST (Film erstellen):
curl -X POST http://127.0.0.1:8000/api/movies/ \
-H "Content-Type: application/json" \
-d '{
"title": "Inception",
"year": 2010,
"genre": "Sci-Fi",
"rating": "8.8"
}'
# PUT (Film aktualisieren):
curl -X PUT http://127.0.0.1:8000/api/movies/1/ \
-H "Content-Type: application/json" \
-d '{
"title": "The Matrix Reloaded",
"year": 2003,
"genre": "Sci-Fi",
"rating": "7.2"
}'
# DELETE:
curl -X DELETE http://127.0.0.1:8000/api/movies/1/
# Installation:
pip install httpie
# GET Liste:
http GET http://127.0.0.1:8000/api/movies/
# GET Details:
http GET http://127.0.0.1:8000/api/movies/1/
# POST (Film erstellen):
http POST http://127.0.0.1:8000/api/movies/ \
title="Inception" \
year=2010 \
genre="Sci-Fi" \
rating=8.8
# PATCH (Teilweise Update):
http PATCH http://127.0.0.1:8000/api/movies/1/ \
rating=9.0
# DELETE:
http DELETE http://127.0.0.1:8000/api/movies/1/
# Custom Action:
http GET http://127.0.0.1:8000/api/movies/top_rated/
# Filtering:
http GET http://127.0.0.1:8000/api/movies/ \
year==2010 \
ordering==-rating
GET /api/movies/ # Liste aller Filme
POST /api/movies/ # Film erstellen
GET /api/movies/{id}/ # Film-Details
PUT /api/movies/{id}/ # Film aktualisieren (komplett)
PATCH /api/movies/{id}/ # Film aktualisieren (teilweise)
DELETE /api/movies/{id}/ # Film löschen
# Custom Actions:
GET /api/movies/top_rated/ # Top 10 Filme
GET /api/movies/recent/ # Filme der letzten 5 Jahre
GET /api/movies/{id}/castings/ # Besetzung eines Films
# Filtering:
GET /api/movies/?year=2010 # Jahr-Filter
GET /api/movies/?genre=Sci-Fi # Genre-Filter
GET /api/movies/?search=Matrix # Textsuche
GET /api/movies/?ordering=-rating # Sortierung
GET /api/artists/ # Liste aller Künstler
POST /api/artists/ # Künstler erstellen
GET /api/artists/{id}/ # Künstler-Details
PUT /api/artists/{id}/ # Künstler aktualisieren
PATCH /api/artists/{id}/ # Künstler aktualisieren (teilweise)
DELETE /api/artists/{id}/ # Künstler löschen
# Custom Actions:
GET /api/artists/{id}/filmography/ # Alle Rollen eines Künstlers
GET /api/artists/{id}/movies/ # Alle Filme eines Künstlers
GET /api/castings/ # Liste aller Besetzungen
POST /api/castings/ # Besetzung erstellen
GET /api/castings/{id}/ # Besetzung-Details
PUT /api/castings/{id}/ # Besetzung aktualisieren
PATCH /api/castings/{id}/ # Besetzung aktualisieren (teilweise)
DELETE /api/castings/{id}/ # Besetzung löschen
# settings.py:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
}
# Installation:
pip install djangorestframework-simplejwt
# JWT für Token-basierte Auth
# Installation:
pip install django-cors-headers
# settings.py:
INSTALLED_APPS = [
# ...
'corsheaders',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
# ...
]
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000",
"http://localhost:8080",
]
# Installation:
pip install drf-spectacular
# settings.py:
INSTALLED_APPS = [
# ...
'drf_spectacular',
]
REST_FRAMEWORK = {
'DEFAULT_SCHEMA_CLASS':
'drf_spectacular.openapi.AutoSchema',
}
# urls.py:
from drf_spectacular.views import (
SpectacularAPIView,
SpectacularSwaggerView
)
urlpatterns = [
path('api/schema/', SpectacularAPIView.as_view()),
path('api/docs/', SpectacularSwaggerView.as_view()),
]
# settings.py:
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle',
],
'DEFAULT_THROTTLE_RATES': {
'anon': '100/day',
'user': '1000/day',
}
}
select_related() für ForeignKeysprefetch_related() für M2MAPITestCase nutzen/api/v1/# filepath: movies/serializers.py
from rest_framework import serializers
from django.core.exceptions import ValidationError
class MovieSerializer(serializers.ModelSerializer):
class Meta:
model = Movie
fields = '__all__'
read_only_fields = ['created_at', 'updated_at']
def validate_year(self, value):
"""Validierung für year-Feld"""
if value < 1888: # Erster Film 1888
raise serializers.ValidationError(
"Jahr kann nicht vor 1888 sein!"
)
if value > 2100:
raise serializers.ValidationError(
"Jahr kann nicht in ferner Zukunft sein!"
)
return value
def validate_rating(self, value):
"""Validierung für rating-Feld"""
if value < 0 or value > 10:
raise serializers.ValidationError(
"Rating muss zwischen 0 und 10 sein!"
)
return value
def validate(self, data):
"""Objekt-Level Validierung (mehrere Felder)"""
if data.get('year') and data.get('rating'):
# Beispiel: Alte Filme haben oft niedrigere Ratings
if data['year'] < 1950 and data['rating'] > 9.0:
raise serializers.ValidationError(
"Sehr alte Filme haben selten so hohe Ratings!"
)
return data
# filepath: movieapi/settings.py
REST_FRAMEWORK = {
# ...existing...
'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler',
'NON_FIELD_ERRORS_KEY': 'error',
}
# filepath: movieapi/settings.py
# DEBUG aus!
DEBUG = False
# ALLOWED_HOSTS setzen
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']
# SECRET_KEY aus Environment Variable
import os
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')
# HTTPS erzwingen
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# REST Framework Production Settings
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated', # Auth required!
],
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer', # Nur JSON, kein Browsable API
],
}
# settings.py:
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
# Terminal:
python manage.py collectstatic
# Terminal:
pip freeze > requirements.txt
# requirements.txt:
Django==4.2.7
djangorestframework==3.14.0
django-filter==23.5
django-cors-headers==4.3.1
# ...
# Production Server:
python manage.py migrate
python manage.py createsuperuser
Eine vollständige REST-API für Movies, Artists & Castings
Unit Tests schreiben
APITestCase nutzen
Coverage > 80%
JWT Tokens
Login/Logout
User Permissions
React/Vue Integration
CORS konfigurieren
API Consumer bauen
Docker Container
CI/CD Pipeline
Production Server
movieapi/
├── movieapi/ # Projekt-Konfiguration
│ ├── __init__.py
│ ├── settings.py # ✅ DRF konfiguriert
│ ├── urls.py # ✅ /api/ Route
│ ├── asgi.py
│ └── wsgi.py
│
├── movies/ # App
│ ├── migrations/
│ │ ├── __init__.py
│ │ ├── 0001_initial.py
│ │ └── ...
│ ├── __init__.py
│ ├── admin.py # ✅ Admin konfiguriert
│ ├── apps.py
│ ├── models.py # ✅ Movie, Artist, MovieCasting
│ ├── serializers.py # ✅ NEU! 4 Serializers
│ ├── views.py # ✅ 3 ViewSets + Custom Actions
│ ├── urls.py # ✅ NEU! Router konfiguriert
│ └── tests.py # TODO: Tests schreiben
│
├── venv/ # Virtuelle Umgebung
├── db.sqlite3 # Datenbank
├── manage.py
└── requirements.txt # ✅ Dependencies
Django==4.2.7
djangorestframework==3.14.0
django-filter==23.5
django-cors-headers==4.3.1 # Optional (für Frontend)
drf-spectacular==0.27.0 # Optional (API Docs)
httpie==3.2.2 # Optional (Testing)
class Movie(models.Model):
# Felder:
title = models.CharField(max_length=200) # Pflichtfeld
year = models.IntegerField() # Pflichtfeld
genre = models.CharField(blank=True) # Optional
rating = models.DecimalField(null=True) # Optional
description = models.TextField(blank=True) # Optional
# Timestamps (automatisch):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# Meta:
class Meta:
ordering = ['-year', 'title'] # Sortierung: Neueste zuerst
# String-Repräsentation:
def __str__(self):
return f"{self.title} ({self.year})"
# API nutzt:
# - Auto-Timestamps für created_at/updated_at
# - ordering für Standard-Sortierung in API
# - __str__ für Admin & Debug
class Artist(models.Model):
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
birth_date = models.DateField(null=True, blank=True)
nationality = models.CharField(blank=True)
biography = models.TextField(blank=True)
# Property (nicht in DB gespeichert):
@property
def full_name(self):
return f"{self.first_name} {self.last_name}"
# API-Nutzung:
# - full_name Property via Serializer verfügbar
# - ReadOnlyField() im ArtistSerializer
class MovieCasting(models.Model):
# ForeignKeys (Beziehungen):
movie = models.ForeignKey(Movie,
on_delete=models.CASCADE,
related_name='castings') # movie.castings.all()
artist = models.ForeignKey(Artist,
on_delete=models.CASCADE,
related_name='movie_roles') # artist.movie_roles.all()
# Zusatz-Felder:
role_name = models.CharField(max_length=200)
is_main_role = models.BooleanField(default=False)
order = models.IntegerField(default=0)
# Unique Constraint:
class Meta:
unique_together = [['movie', 'artist', 'role_name']]
# Gleicher Artist kann nicht 2x gleiche Rolle im selben Film
# API-Nutzung:
# - related_name für reverse Queries
# - movie.castings.all() für /movies/{id}/castings/
# - artist.movie_roles.all() für /artists/{id}/filmography/
class MovieSerializer(serializers.ModelSerializer):
class Meta:
model = Movie
fields = '__all__' # Alle Felder aus Model
read_only_fields = ['created_at', 'updated_at'] # Nicht änderbar
# Automatisch verfügbar:
# - id, title, year, genre, rating, description, created_at, updated_at
# - Validierung basierend auf Model-Definitionen
# - to_representation() für JSON-Serialisierung
# - to_internal_value() für JSON-Deserialisierung
class ArtistSerializer(serializers.ModelSerializer):
# Property aus Model als ReadOnlyField:
full_name = serializers.ReadOnlyField()
class Meta:
model = Artist
fields = '__all__'
read_only_fields = ['created_at', 'updated_at']
# JSON Output:
# {
# "id": 1,
# "first_name": "Keanu",
# "last_name": "Reeves",
# "full_name": "Keanu Reeves", ← Property!
# "birth_date": "1964-09-02",
# "nationality": "Canadian",
# ...
# }
class MovieCastingSerializer(serializers.ModelSerializer):
class Meta:
model = MovieCasting
fields = '__all__'
read_only_fields = ['created_at']
# JSON Output (nur IDs):
# {
# "id": 1,
# "movie": 1, ← Nur ID!
# "artist": 1, ← Nur ID!
# "role_name": "Neo",
# "is_main_role": true,
# "order": 1
# }
class MovieCastingDetailSerializer(serializers.ModelSerializer):
# Verschachtelte Serializer:
movie = MovieSerializer(read_only=True)
artist = ArtistSerializer(read_only=True)
class Meta:
model = MovieCasting
fields = '__all__'
read_only_fields = ['created_at']
# JSON Output (vollständige Objekte):
# {
# "id": 1,
# "movie": { ← Vollständiges Movie-Objekt!
# "id": 1,
# "title": "The Matrix",
# "year": 1999,
# ...
# },
# "artist": { ← Vollständiges Artist-Objekt!
# "id": 1,
# "first_name": "Keanu",
# "last_name": "Reeves",
# "full_name": "Keanu Reeves",
# ...
# },
# "role_name": "Neo",
# "is_main_role": true,
# "order": 1
# }
# Verwendung:
# - In Custom Action: /movies/{id}/castings/
# - In Custom Action: /artists/{id}/filmography/
class MovieViewSet(viewsets.ModelViewSet):
# Basis-Konfiguration:
queryset = Movie.objects.all()
serializer_class = MovieSerializer
# Filter-Backends:
filter_backends = [
DjangoFilterBackend, # ?year=2010&genre=Sci-Fi
filters.SearchFilter, # ?search=Matrix
filters.OrderingFilter # ?ordering=-rating
]
# Welche Felder filtern?
filterset_fields = ['year', 'genre']
# → GET /api/movies/?year=2010
# → GET /api/movies/?genre=Sci-Fi
# → GET /api/movies/?year=2010&genre=Action
# Welche Felder durchsuchen?
search_fields = ['title', 'description']
# → GET /api/movies/?search=Matrix
# → Sucht in title UND description
# Welche Felder sortieren?
ordering_fields = ['year', 'rating', 'title']
# → GET /api/movies/?ordering=year (aufsteigend)
# → GET /api/movies/?ordering=-rating (absteigend)
# → GET /api/movies/?ordering=-year,title (mehrfach)
# Standard-Sortierung (ohne ?ordering=...):
ordering = ['-year']
# → Neueste Filme zuerst
# Automatisch verfügbare Endpoints:
# - list() GET /api/movies/
# - create() POST /api/movies/
# - retrieve() GET /api/movies/{id}/
# - update() PUT /api/movies/{id}/
# - partial_update() PATCH /api/movies/{id}/
# - destroy() DELETE /api/movies/{id}/
# @action Decorator Parameter erklärt:
#
# detail (bool) - PFLICHT:
# • False = Collection-Aktion (ohne ID)
# URL: /api/movies/top_rated/
# • True = Objekt-Aktion (mit ID)
# URL: /api/movies/{id}/castings/
#
# methods (list) - PFLICHT:
# • ['get'] = Nur GET erlaubt
# • ['post'] = Nur POST erlaubt
# • ['get', 'post'] = Beide erlaubt
# Mögliche Werte: 'get', 'post', 'put', 'patch', 'delete'
#
# url_path (str) - OPTIONAL:
# • Standard: Methodenname
# • Beispiel: url_path='top-rated'
# → URL wird /api/movies/top-rated/ statt /api/movies/top_rated/
#
# url_name (str) - OPTIONAL:
# • Standard: Methodenname
# • Für reverse() in Django
#
# Weitere Parameter (alle OPTIONAL):
# - serializer_class = MySerializer
# → Anderer Serializer nur für diese Action
# - permission_classes = [IsAuthenticated]
# → Andere Permissions nur für diese Action
# - authentication_classes = [TokenAuthentication]
# → Andere Auth nur für diese Action
# - throttle_classes = [AnonRateThrottle]
# → Rate Limiting nur für diese Action
# - pagination_class = None
# → Pagination deaktivieren für diese Action
# - filter_backends = [...]
# → Andere Filter nur für diese Action
@action(detail=False, methods=['get'])
def top_rated(self, request):
"""
Collection-Aktion (detail=False)
URL: GET /api/movies/top_rated/
Keine ID in URL benötigt.
Gibt Liste der Top 10 Filme zurück.
"""
# Filter: Nur Filme mit Rating
# Sortierung: Rating absteigend
# Limit: Erste 10
movies = Movie.objects.filter(
rating__isnull=False
).order_by('-rating')[:10]
# Serializer mit many=True für Liste
serializer = self.get_serializer(movies, many=True)
# Response mit JSON
return Response(serializer.data)
@action(detail=False, methods=['get'])
def recent(self, request):
"""
Collection-Aktion (detail=False)
URL: GET /api/movies/recent/
Filme der letzten 5 Jahre.
"""
from datetime import datetime
current_year = datetime.now().year
# Filter: year >= aktuelles Jahr - 5
movies = Movie.objects.filter(year__gte=current_year - 5)
serializer = self.get_serializer(movies, many=True)
return Response(serializer.data)
@action(detail=True, methods=['get'])
def castings(self, request, pk=None):
"""
Objekt-Aktion (detail=True)
URL: GET /api/movies/{id}/castings/
ID in URL erforderlich!
Gibt Besetzung eines Films zurück.
"""
# self.get_object() lädt Movie anhand pk
# Wirft 404 wenn nicht gefunden
movie = self.get_object()
# Related Manager: movie.castings (aus related_name)
castings = movie.castings.all()
# Detaillierter Serializer mit Movie & Artist verschachtelt
serializer = MovieCastingDetailSerializer(castings, many=True)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def add_to_favorites(self, request, pk=None):
"""
Objekt-Aktion mit POST (detail=True, methods=['post'])
URL: POST /api/movies/{id}/add_to_favorites/
Ändert State (Favoriten).
"""
movie = self.get_object()
# Beispiel-Logik (Platzhalter):
# In Realität: request.user.favorites.add(movie)
# Oder: Favorite.objects.create(user=request.user, movie=movie)
# Response mit Status-Info
return Response({
'status': 'success',
'message': 'Film zu Favoriten hinzugefügt',
'movie': {
'id': movie.id,
'title': movie.title
}
}, status=status.HTTP_200_OK)
class ArtistViewSet(viewsets.ModelViewSet):
queryset = Artist.objects.all()
serializer_class = ArtistSerializer
@action(detail=True, methods=['get'])
def filmography(self, request, pk=None):
"""
Objekt-Aktion (detail=True)
URL: GET /api/artists/{id}/filmography/
Liefert alle Rollen eines Künstlers
mit vollständigen Movie & Artist Objekten.
"""
# Künstler laden
artist = self.get_object()
# Related Manager: artist.movie_roles (aus related_name)
# → Alle MovieCasting-Objekte für diesen Artist
castings = artist.movie_roles.all()
# Detaillierter Serializer
serializer = MovieCastingDetailSerializer(castings, many=True)
# JSON Response:
# [
# {
# "id": 1,
# "movie": { "id": 1, "title": "The Matrix", ... },
# "artist": { "id": 1, "full_name": "Keanu Reeves", ... },
# "role_name": "Neo",
# "is_main_role": true,
# "order": 1
# },
# {
# "id": 5,
# "movie": { "id": 3, "title": "John Wick", ... },
# "artist": { "id": 1, "full_name": "Keanu Reeves", ... },
# "role_name": "John Wick",
# "is_main_role": true,
# "order": 1
# }
# ]
return Response(serializer.data)
@action(detail=True, methods=['get'])
def movies(self, request, pk=None):
"""
Objekt-Aktion (detail=True)
URL: GET /api/artists/{id}/movies/
Liefert nur die Filme (ohne Casting-Details).
Einfachere Alternative zu filmography.
"""
artist = self.get_object()
# Query über MovieCasting-Beziehung
# distinct() vermeidet Duplikate (wenn Artist mehrere Rollen im Film)
movies = Movie.objects.filter(
castings__artist=artist
).distinct()
# Nur Movie-Serializer (nicht MovieCastingDetailSerializer)
serializer = MovieSerializer(movies, many=True)
# JSON Response:
# [
# { "id": 1, "title": "The Matrix", "year": 1999, ... },
# { "id": 3, "title": "John Wick", "year": 2014, ... }
# ]
return Response(serializer.data)
# 1. Sci-Fi Filme ab 2010, sortiert nach Rating, nur Top 5
GET /api/movies/?genre=Sci-Fi&year__gte=2010&ordering=-rating&page_size=5
# Query-Parameter erklärt:
# - genre=Sci-Fi → Filterset: genre field
# - year__gte=2010 → Django ORM Lookup: year >= 2010
# - ordering=-rating → OrderingFilter: Rating absteigend
# - page_size=5 → Pagination: nur 5 Ergebnisse
# 2. Suche nach "Matrix" in Title/Description
GET /api/movies/?search=Matrix
# SearchFilter durchsucht:
# - title (icontains)
# - description (icontains)
# 3. Künstler mit "Reeves" im Namen
GET /api/artists/?search=Reeves
# Findet:
# - first_name oder last_name enthält "Reeves"
# 4. Kombiniert: Sci-Fi Filme, Suche "space", sortiert Jahr
GET /api/movies/?genre=Sci-Fi&search=space&ordering=-year
# Logik:
# 1. Filter: genre = Sci-Fi
# 2. Search: "space" in title OR description
# 3. Order: year absteigend (neueste zuerst)
# Top 10 Filme
GET /api/movies/top_rated/
# Response: [{ "id": 1, "title": "...", "rating": "9.5" }, ...]
# Filme der letzten 5 Jahre
GET /api/movies/recent/
# Response: [{ "id": 5, "year": 2023, ... }, ...]
# Besetzung von Film ID 1
GET /api/movies/1/castings/
# Response: [
# {
# "id": 1,
# "movie": { "id": 1, "title": "The Matrix", ... },
# "artist": { "id": 1, "full_name": "Keanu Reeves", ... },
# "role_name": "Neo",
# "is_main_role": true
# }
# ]
# Filmographie von Artist ID 1
GET /api/artists/1/filmography/
# Response: Alle Rollen von Keanu Reeves
# Nur Filme von Artist ID 1
GET /api/artists/1/movies/
# Response: Nur Film-Objekte (ohne Casting-Details)
select_related & prefetch_related nutzen
# views.py - SCHLECHT:
@action(detail=True, methods=['get'])
def castings(self, request, pk=None):
movie = self.get_object()
castings = movie.castings.all() # 1 Query
serializer = MovieCastingDetailSerializer(
castings, many=True
)
return Response(serializer.data)
# Problem:
# 1 Query: movie.castings.all()
# N Queries: Für jedes Casting → movie laden
# N Queries: Für jedes Casting → artist laden
#
# Bei 10 Castings: 1 + 10 + 10 = 21 Queries!
# → Sehr langsam!
# views.py - GUT:
@action(detail=True, methods=['get'])
def castings(self, request, pk=None):
movie = self.get_object()
# select_related() für ForeignKeys:
castings = movie.castings.select_related(
'movie', # ForeignKey
'artist' # ForeignKey
).all()
serializer = MovieCastingDetailSerializer(
castings, many=True
)
return Response(serializer.data)
# Optimiert:
# 1 Query mit JOINs:
# SELECT * FROM movie_casting
# INNER JOIN movies ON (movie_casting.movie_id = movies.id)
# INNER JOIN artists ON (movie_casting.artist_id = artists.id)
# WHERE movie_casting.movie_id = 1
#
# Bei 10 Castings: nur 1 Query!
# → Sehr schnell!
# 1. Queryset auf ViewSet-Level optimieren:
class MovieViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.select_related('genre').all()
serializer_class = MovieSerializer
# Alle Movie-Queries nutzen jetzt select_related()
# 2. prefetch_related() für Reverse ForeignKeys & M2M:
class MovieViewSet(viewsets.ModelViewSet):
queryset = Movie.objects.prefetch_related('castings').all()
@action(detail=True, methods=['get'])
def castings(self, request, pk=None):
movie = self.get_object()
# castings sind bereits geprefetched!
castings = movie.castings.select_related(
'artist'
).all()
serializer = MovieCastingDetailSerializer(
castings, many=True
)
return Response(serializer.data)
# 3. only() / defer() für große Felder:
class MovieViewSet(viewsets.ModelViewSet):
def list(self, request):
# Nur benötigte Felder laden (nicht description):
movies = Movie.objects.only(
'id', 'title', 'year', 'rating'
).all()
serializer = self.get_serializer(movies, many=True)
return Response(serializer.data)
# 4. Pagination nutzen (bereits konfiguriert):
# → Nur 10 Einträge pro Request
# → Verhindert zu große Responses
# 5. Database Indexing:
class Movie(models.Model):
year = models.IntegerField(db_index=True) # Index!
genre = models.CharField(db_index=True) # Index!
class Meta:
indexes = [
models.Index(fields=['year', 'genre']), # Composite Index
]
# settings.py (Production):
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticatedOrReadOnly',
],
}
# ViewSet-Level:
class MovieViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated]
# Action-Level:
@action(detail=True,
methods=['post'],
permission_classes=[IsAdminUser])
def delete_permanently(self, request, pk=None):
# Nur Admins können löschen
pass
# serializers.py:
class MovieSerializer(serializers.ModelSerializer):
def validate_year(self, value):
if value < 1888:
raise ValidationError(
"Jahr kann nicht vor 1888 sein!"
)
return value
def validate(self, data):
# Multi-Field Validierung
if data['year'] > 2100:
raise ValidationError(
"Zu weit in Zukunft!"
)
return data
# settings.py:
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle',
],
'DEFAULT_THROTTLE_RATES': {
'anon': '100/day',
'user': '1000/day',
}
}
# Custom Throttle:
class BurstRateThrottle(UserRateThrottle):
rate = '60/min'
# settings.py:
INSTALLED_APPS = [
'corsheaders',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
]
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000",
"https://yourdomain.com",
]
# settings.py:
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# settings.py:
LOGGING = {
'version': 1,
'handlers': {
'file': {
'level': 'ERROR',
'class': 'logging.FileHandler',
'filename': 'errors.log',
},
},
'loggers': {
'django': {
'handlers': ['file'],
'level': 'ERROR',
},
},
}
Happy API Building! 🚀